import { select } from 'd3-selection'
import { drag as d3Drag } from 'd3-drag'
import rough from 'roughjs'
import mitter from '@/mitt'
import markerShape from '../marker-shape'
import shape from '../shape'
import { getTopicNodeRect } from '../utils/size'
import { getLatextSize, getLatexHtmlString } from '../utils/katex'

let svgEl = null
let mindOutContainer = null
let container = null
let rc = null
const alignMap = {
  'flex-start': 'left',
  center: 'center',
  'flex-end': 'right'
}
export function graphNodeContainer (svg, mindContainer) {
  svgEl = svg
  mindOutContainer = mindContainer
  rc = rough.svg(mindContainer)
  container = mindContainer
    .append('g')
    .attr('class', 'mind-map-nodebox')
  return container
}

/**
 * 绘制xmind画布中新增的所有节点信息元素
 * @param {*} nodes
 */
export function renderNewAllElementNodes (nodes, skeletonTheme) {
  const isRough = Boolean(Number(sessionStorage.getItem('isRough')))
  const enter = container
    .selectAll('.x-mind-nodetheme')
    .data(nodes)
    .enter()
    .append('g')
    .attr('id', d => d.data._id)
    .attr('class', d => `x-mind-nodetheme ${d.data.isRoot ? 'x-mind-root-theme' : 'x-mind-ref-theme'}`)
    .attr('fill', 'transparent')
    .call(
      d3Drag()
        .on('drag', function (event) {
          if (event.dx !== 0 || event.dy !== 0) {
            mitter.emit('node-handler-draging', { event, _this: this, isRough })
          }
        })
        .on('end', function (event) {
          mitter.emit('node-handler-dragend', { event, _this: this })
        }))
    .on('click', function (event) {
      mitter.emit('node-handler-click', { event, _this: this })
    })
    .on('contextmenu', function (event) {
      mitter.emit('node-context-click', { event, _this: this })
    })
    .on('dblclick', function (event) {
      mitter.emit('node-handler-dblclick', { event, _this: this })
    })
    .on('mouseenter', function (event) {
      const svgClassNames = svgEl.attr('class')
      if (svgClassNames?.includes('n-resize') || svgClassNames?.includes('e-resize')) return
      mitter.emit('node-handler-mouseenter', { event, _this: this })
      showNodeHoverBorderPath(select(this))
    })
    .on('mouseleave', function (event) {
      mitter.emit('node-handler-mouseleave', { event, _this: this })
      hiddenNodeHoverBorderPath(select(this))
    })

  if (isRough) {
    drawNewRoughNodes(enter, skeletonTheme)
  } else {
    drawNewNodes(enter)
  }
  drawNewForeignObject(enter)
  drawNodeArrowMarkers()
  drawNodeTask()
  drawSerialNumber()
}

/**
 * 绘制新的手绘效果的主题背景和边框
 * @param {*} enter
 */
function drawNewRoughNodes (enter, skeletonTheme) {
  enter.append(function (d) {
    const path = shape[d.style.shape || 'default'].generateShapePath(d.x, d.y, d.width, d.height, d.style.strokeWidth)
    const node = select(rc.path(typeof path === 'string' ? path : path[0], {
      fill: d.style.fill,
      stroke: d.style.stroke,
      strokeWidth: d.style.strokeWidth,
      ...skeletonTheme.nodeRoughOptions
    }))
    node.insert('path', 'path')
      .attr('class', 'invalid-path')
      .attr('d', typeof path === 'string' ? path : path[1])
      .attr('fill', typeof path === 'string' ? 'none' : d.style.fill)
    return node.node()
  })
    .attr('class', 'x-mind-node-block')
}

/**
 * 绘制普通效果的主题背景和边框
 * @param {*} enter
 */
function drawNewNodes (enter) {
  enter
    .append('g')
    .attr('class', 'x-mind-node-block')
    .append('path')
    .attr('class', 'valid-path')
    .attr('d', function (d) {
      const path = shape[d.style.shape || 'default'].generateShapePath(d.x, d.y, d.width, d.height, d.style.strokeWidth)
      if (typeof path === 'string') return path
      select(this.parentNode).insert('path', 'path').attr('class', 'invalid-path').attr('d', path[1]).attr('fill', d.style.fill)
      return path[0]
    })
    .attr('fill', d => d.style.fill)
    .attr('stroke', d => d.style.stroke)
    .attr('stroke-width', d => d.style.strokeWidth)
    .attr('stroke-linecap', d => d.style.strokeLineCap || 'round')
    .attr('stroke-dasharray', d => d.style.strokeStyle === 'dashed' ? `8, ${8 + d.style.strokeWidth - 2}` : undefined)
}

/**
 * 绘制新的主题节点文字描述信息
 * @param {*} enter
 */
function drawNewForeignObject (enter) {
  enter
    .append('foreignObject')
    .attr('class', 'x-mind-node-text')
    .attr('width', d => d.data.foreignObjectWidth)
    .attr('height', d => d.data.foreignObjectHeight)
    .attr('x', d => {
      const taskSpacing = d.data.isTask ? d.style.linkSize + 8 : 0
      const marks = d.data.marks
      if (marks?.length) {
        return d.x + d.style.margin._l + d.markWidth + taskSpacing + (d.serialNumberWidth || 0)
      }
      return d.x + d.style.margin._l + taskSpacing + (d.serialNumberWidth || 0)
    })
    .attr('y', d => d.y + d.height - d.data.foreignObjectHeight - d.style.margin._b - d.tagLineHeight)
    .append('xhtml:div')
    .attr('xmlns', 'http://www.w3.org/1999/xhtml')
    .attr('class', 'for-block')
    .style('display', 'flex')
    .style('align-items', 'center')
    .style('height', d => d.data.foreignObjectHeight + 'px')
    .style('color', d => d.style.textStyle.color)
    .style('font-size', d => d.style.textStyle.fontSize + 'px')
    .style('justify-content', d => d.style.textStyle.align)
    .append('xhtml:div')
    .attr('class', 'node-text-description')
    .text(d => d.data.text)
    .style('font-weight', d => d.style.textStyle.fontWeight)
    .style('font-style', d => d.style.textStyle.fontStyle)
    .style('font-family', d => d.style.textStyle.fontFamily)
    .style('text-decoration', d => d.style.textStyle.textDecoration)
    .style('writing-mode', d => d.style.textStyle.textDirection === 'ver' ? 'vertical-lr' : 'horizontal-tb')
    .style('line-break', d => d.style.textStyle.textDirection === 'ver' ? 'auto' : 'anywhere')
    .style('word-break', d => d.style.textStyle.textDirection === 'ver' ? 'keep-all' : 'break-all')
    .style('display', 'block')
    .style('text-align', d => alignMap[d.style.textStyle.align])
    .each(function (d) {
      if (d.data.latex) {
        select(this.parentNode).append('xhtml:div')
          .attr('class', 'latex-span')
          .html(getLatexHtmlString(d.data.latex))
          .select('.katex').style('display', 'flex')
      }
    })
}

function drawSerialNumber () {
  container.selectAll('.node-serial-numner').remove()
  container
    .selectAll('.x-mind-nodetheme')
    .filter(node => node.serialNumber)
    .append('foreignObject')
    .attr('class', 'node-serial-numner')
    .attr('width', d => d.serialNumberWidth)
    .attr('height', d => d.serialNumberHeight)
    .attr('x', d => {
      const taskSpacing = d.data.isTask ? d.style.linkSize + 8 : 0
      const markWidth = d.markWidth || 0
      return d.x + d.style.margin._l + taskSpacing + markWidth
    })
    .attr('y', d => {
      return d.y + d.height - (d.data.foreignObjectHeight + d.serialNumberHeight) / 2 - d.style.margin._b - d.tagLineHeight
    })
    .append('xhtml:p')
    .attr('xmlns', 'http://www.w3.org/1999/xhtml')
    .text(d => d.serialNumber)
    .style('color', d => d.style.textStyle.color)
    .style('font-size', d => d.style.textStyle.fontSize + 'px')
    .style('font-weight', d => d.style.textStyle.fontWeight)
    .style('font-style', d => d.style.textStyle.fontStyle)
    .style('font-family', d => d.style.textStyle.fontFamily)
    .style('margin', 0)
}

/**
 * 主题节点中的连线箭头marker定义
 */
function drawNodeArrowMarkers () {
  const isRough = sessionStorage.getItem('isRough') === '1'
  container.selectAll('.x-mind-nodetheme').select('marker').remove()
  container
    .selectAll('.x-mind-nodetheme')
    .filter(node => {
      const parent = node.parent
      const parentIsRoot = parent?.data.isRoot
      return parent &&
      (
        (node.style.lineStyle.lineEndJoin && node.style.lineStyle.lineEndJoin !== 'arrow-none') ||
        (parentIsRoot && parent.style.lineStyle.lineEndJoin && parent.style.lineStyle.lineEndJoin !== 'arrow-none')
      )
    })
    .append('marker')
    .attr('id', d => `mark-triangle-${d.data._id}`)
    .attr('viewBox', d => {
      const parent = d.parent
      const lineEndJoin = d.style.lineStyle.lineEndJoin || parent?.style.lineStyle.lineEndJoin
      return markerShape[lineEndJoin].viewBox
    })
    .attr('refX', d => {
      const parent = d.parent
      const lineEndJoin = d.style.lineStyle.lineEndJoin || parent?.style.lineStyle.lineEndJoin
      return markerShape[lineEndJoin].refX
    })
    .attr('refY', d => {
      const parent = d.parent
      const lineEndJoin = d.style.lineStyle.lineEndJoin || parent?.style.lineStyle.lineEndJoin
      return markerShape[lineEndJoin].refY
    })
    .attr('markerUnits', 'strokeWidth')
    .attr('markerWidth', d => {
      const parent = d.parent
      const lineEndJoin = d.style.lineStyle.lineEndJoin || parent?.style.lineStyle.lineEndJoin
      return markerShape[lineEndJoin].markerWidth
    })
    .attr('markerHeight', d => {
      const parent = d.parent
      const lineEndJoin = d.style.lineStyle.lineEndJoin || parent?.style.lineStyle.lineEndJoin
      return markerShape[lineEndJoin].markerHeight
    })
    .attr('orient', 'auto')
    .append((d) => {
      const parent = d.parent
      const lineEndJoin = d.style.lineStyle.lineEndJoin || parent?.style.lineStyle.lineEndJoin
      const node = rc.path(markerShape[lineEndJoin].d, {
        fillStyle: 'zigzag',
        fillWeight: 1,
        hachureGap: 0.5,
        strokeWidth: 0.5,
        bowing: 1,
        roughness: 0.5,
        seed: 64,
        stroke: d.style.lineStyle.fill,
        fill: d.style.lineStyle.fill
      })
      return isRough ? node : select('g').append('path').attr('d', () => {
        const parent = d.parent
        const lineEndJoin = d.style.lineStyle.lineEndJoin || parent?.style.lineStyle.lineEndJoin
        return markerShape[lineEndJoin].d
      }).attr('fill', d.style.lineStyle.fill).node()
    })
}

/**
 * 主题节点中的task任务节点绘制
 */
function drawNodeTask () {
  container.selectAll('.x-mind-nodetheme').select('.task-checkbox').remove()
  container.selectAll('.x-mind-nodetheme')
    .filter(node => node.data.isTask)
    .append('g')
    .attr('class', 'task-checkbox')
    .on('click', function (event) {
      mitter.emit('handler-click-task-checkbox', { event, _this: this })
    })
    .on('dblclick', event => event.stopPropagation())
    .attr('transform', 'translate(0, 0)')
    .append('rect')
    .attr('x', d => d.x + d.style.margin._l)
    .attr('y', d => d.y + d.height - d.data.foreignObjectHeight / 2 - d.style.margin._b - d.tagLineHeight - 7)
    .attr('rx', 2)
    .attr('ry', 2)
    .attr('width', 15)
    .attr('height', 15)
    .attr('stroke', '#620703')
    .attr('stroke-width', 1.5)
    .attr('fill', d => d.data.taskValue ? '#620703' : 'transparent')
    .each(function () {
      select(this.parentNode).append('path')
        .attr('stroke-width', 2)
        .attr('stroke', '#fff')
        .attr('d', d => {
          const x = d.x + d.style.margin._l
          const y = d.y + d.height - d.data.foreignObjectHeight / 2 - d.style.margin._b - d.tagLineHeight - 7
          return `M${x + 3} ${y + 7} L${x + 7} ${y + 10} ${x + 11} ${y + 3}`
        })
        .attr('fill', 'none')
        .style('display', d => d.data.taskValue ? 'block' : 'none')
    })
}

export function updateExistAllElementNodes (nodes, skeletonTheme) {
  const isRough = Boolean(Number(sessionStorage.getItem('isRough')))
  if (isRough) {
    updateRoughNodes(nodes, skeletonTheme)
  } else {
    updateNodes(nodes)
  }
  updateForeignObject(nodes)
}

/**
 * 更新手绘模式的主题背景和边框
 * @param {*} nodes
 */
function updateRoughNodes (nodes, skeletonTheme) {
  select('.mind-map-nodebox')
    .selectAll('.x-mind-nodetheme')
    .data(nodes)
    .attr('id', d => d.data._id)
    .classed('x-mind-root-theme', d => d.data.isRoot)
    .classed('x-mind-ref-theme', d => !d.data.isRoot)
    .attr('transform', null)
    .select('.x-mind-node-block')
    .html(function (d) {
      select(this).select('.invalid-path').remove()
      const path = shape[d.style.shape || 'default'].generateShapePath(d.x, d.y, d.width, d.height, d.style.strokeWidth)
      const node = select(rc.path(typeof path === 'string' ? path : path[0], {
        fill: d.style.fill,
        stroke: d.style.stroke,
        strokeWidth: d.style.strokeWidth,
        ...skeletonTheme.nodeRoughOptions
      }))
      node.insert('path', 'path')
        .attr('class', 'invalid-path')
        .attr('d', typeof path === 'string' ? path : path[1])
        .attr('fill', typeof path === 'string' ? 'none' : d.style.fill)
      return node.html()
    })
}

/**
 * 更新普通节点的背景和边框信息
 * @param {*} nodes
 */
function updateNodes (nodes) {
  select('.mind-map-nodebox')
    .selectAll('.x-mind-nodetheme')
    .data(nodes)
    .attr('id', d => d.data._id)
    .classed('x-mind-root-theme', d => d.data.isRoot)
    .classed('x-mind-ref-theme', d => !d.data.isRoot)
    .attr('transform', null)
    .select('.x-mind-node-block')
    .select('.valid-path')
    .attr('d', function (d) {
      const path = shape[d.style.shape || 'default'].generateShapePath(d.x, d.y, d.width, d.height, d.style.strokeWidth)
      select(this.parentNode).select('.invalid-path').remove()
      if (typeof path === 'string') {
        return shape[d.style.shape || 'default'].generateShapePath(d.x, d.y, d.width, d.height, d.style.strokeWidth)
      }
      select(this.parentNode).insert('path', 'path').attr('class', 'invalid-path').attr('d', path[1]).attr('fill', d.style.fill)
      return path[0]
    })
    .attr('fill', d => d.style.fill)
    .attr('stroke', d => d.style.stroke)
    .attr('stroke-width', d => d.style.strokeWidth)
    .attr('stroke-linecap', d => d.style.strokeLineCap || 'round')
    .attr('stroke-dasharray', d => d.style.strokeStyle === 'dashed' ? `8, ${8 + d.style.strokeWidth - 2}` : undefined)
}

/**
 * 更新主题节点的文字描述信息
 * @param {*} nodes
 */
function updateForeignObject (nodes) {
  select('.mind-map-nodebox')
    .selectAll('.x-mind-nodetheme')
    .data(nodes)
    .select('.x-mind-node-text')
    .attr('width', d => d.data.foreignObjectWidth)
    .attr('height', d => d.data.foreignObjectHeight)
    .attr('x', d => {
      const taskSpacing = d.data.isTask ? d.style.linkSize + 8 : 0
      const marks = d.data.marks
      if (marks?.length) {
        return d.x + d.style.margin._l + d.markWidth + taskSpacing + (d.serialNumberWidth || 0)
      }
      return d.x + d.style.margin._l + taskSpacing + (d.serialNumberWidth || 0)
    })
    .attr('y', d => d.y + d.height - d.data.foreignObjectHeight - d.style.margin._b - d.tagLineHeight)
    .select('.for-block')
    .style('color', d => d.style.textStyle.color)
    .style('font-size', d => d.style.textStyle.fontSize + 'px')
    .style('height', d => d.data.foreignObjectHeight + 'px')
    .style('justify-content', d => d.style.textStyle.align)
    .select('.node-text-description')
    .text(d => d.data.text)
    .style('font-weight', d => d.style.textStyle.fontWeight)
    .style('font-style', d => d.style.textStyle.fontStyle)
    .style('font-family', d => d.style.textStyle.fontFamily)
    .style('text-decoration', d => d.style.textStyle.textDecoration)
    .style('text-align', d => d.style.textStyle.align)
    .style('writing-mode', d => d.style.textStyle.textDirection === 'ver' ? 'vertical-lr' : 'horizontal-tb')
    .style('line-break', d => d.style.textStyle.textDirection === 'ver' ? 'auto' : 'anywhere')
    .style('word-break', d => d.style.textStyle.textDirection === 'ver' ? 'keep-all' : 'break-all')
    .style('display', 'block')
    .style('text-align', d => alignMap[d.style.textStyle.align])
    .each(function (d) {
      select(this.parentNode).select('.latex-span').remove()
      if (d.data.latex) {
        select(this.parentNode).append('xhtml:div')
          .attr('class', 'latex-span')
          .html(getLatexHtmlString(d.data.latex))
          .select('.katex').style('display', 'flex')
      }
    })
}

/**
 * 画布上删除数据中不存在的节点
 */
export function deleteSuperNodes (nodes) {
  container
    .selectAll('.x-mind-nodetheme')
    .data(nodes)
    .exit()
    .remove()
}

/**
 * node节点选中高亮样式设置
 * @param {*} id
 * @param {*} skeletonTheme
 */
export function nodeHighLight (id, skeletonTheme) {
  select(`#${id}`)
    .classed('select-node', true)
    .insert('rect', ':first-child')
    .attr('class', 'select-rect-border')
    .attr('stroke', skeletonTheme.select.stroke)
    .attr('stroke-width', skeletonTheme.select.strokeWidth)
    .attr('x', d => d.x - d.style.strokeWidth / 2 - 3)
    .attr('y', d => d.y - d.style.strokeWidth / 2 - 3)
    .attr('width', d => d.width + (d.style.strokeWidth / 2 + 3) * 2)
    .attr('height', d => d.height + (d.style.strokeWidth / 2 + 3) * 2)
    .attr('rx', 4)
    .attr('ry', 4)
    .attr('fill', 'transparent')
}

/**
 * 主题文字编辑的时候实时更新主题大大小和位置数据
 * @param {*} data
 * @param {*} text
 */
export function realTimeUpdateNode (data, computedStyle, skeletonTheme) {
  const isRough = sessionStorage.getItem('isRough') === '1'
  const id = data.data._id
  const nodeShape = data.style.shape || 'default'
  const isunderline = ['underline', 'doubleUnderline'].includes(nodeShape)
  const { fontFamily, fontSize, fontWeight, maxWidth, fontStyle, text, wordBreak, writingMode, lineBreak, k } = computedStyle
  const { width, height } = getTopicNodeRect({
    fontFamily,
    fontSize: parseInt(fontSize),
    fontWeight,
    maxWidth: `${maxWidth}px`,
    fontStyle,
    text,
    wordBreak,
    writingMode,
    lineBreak
  })
  const { width: latexWidth, height: latexHeight } = data.data.latex ? getLatextSize(parseInt(fontSize), data.data.latex) : { width: 0, height: 0 }
  let [widthRect, heightRect] = [width + latexWidth, Math.max(latexHeight, height)]
  const serialNumberWidth = data.serialNumberWidth || 0
  if (data.data.marks?.length) {
    widthRect = widthRect + data.markWidth
    heightRect = Math.max(heightRect, data.style.markSize)
  }
  if (data.serialNumber) {
    widthRect = widthRect + serialNumberWidth
  }
  if (data.data.link) {
    widthRect += data.style.linkSize + 8
  }
  if (data.data.topiclink) {
    widthRect += data.style.linkSize + 8
  }
  if (data.data.comment) {
    widthRect += data.style.linkSize + 8
  }
  if (data.data.isTask) {
    widthRect = widthRect + data.style.linkSize + 8
  }
  if (data.data.imageInfo) {
    const imageInfo = data.data.imageInfo
    widthRect = Math.max(widthRect, imageInfo.width)
    heightRect = heightRect + imageInfo.height + 8
  }
  if (data.data.tag) {
    widthRect = Math.max(widthRect, data.tagLength)
    heightRect += data.tagLineHeight
  }

  const shapeGetPadding = shape[nodeShape].shapeGetPadding
  const { paddingT = 0, paddingR = 0, paddingB = 0, paddingL = 0 } = shapeGetPadding ? shapeGetPadding(widthRect, heightRect) : {}
  const [spacingT, spacingR, spacingB, spacingL] = [
    paddingT || data.style.margin._t,
    paddingR || data.style.margin._r,
    paddingB || data.style.margin._b,
    paddingL || data.style.margin._l
  ]
  widthRect = widthRect + spacingL + spacingR
  heightRect = heightRect + spacingT + spacingB
  const { top, left } = container.select(`#${id}`).select('.x-mind-node-text').node().getBoundingClientRect()
  if (widthRect > data.width || heightRect > data.height) {
    const offsetX = data.direction === 'left' ? data.width - widthRect : 0
    if (isRough) {
      const path = shape[nodeShape].generateShapePath(
        data.x + offsetX,
        data.y + (data.height - heightRect) / (isunderline ? 1 : 2),
        widthRect,
        heightRect,
        data.style.strokeWidth
      )
      container.select(`#${id}`).select('.x-mind-node-block').html(() => {
        return select(rc.path(typeof path === 'string' ? path : path[0], {
          fill: data.style.fill,
          stroke: data.style.stroke,
          strokeWidth: data.style.strokeWidth,
          ...skeletonTheme?.nodeRoughOptions
        })).html()
      })
      if (typeof path !== 'string') {
        container.select(`#${id}`).select('.x-mind-node-block').select('.invalid-path').attr('d', path[1])
      }
    } else {
      const path = shape[nodeShape].generateShapePath(
        data.x + offsetX,
        data.y + (data.height - heightRect) / (isunderline ? 1 : 2),
        widthRect,
        heightRect,
        data.style.strokeWidth)
      container.select(`#${id}`).select('.x-mind-node-block').select('.valid-path').attr('d', () => {
        return typeof path === 'string' ? path : path[0]
      })
      if (typeof path !== 'string') {
        container.select(`#${id}`).select('.x-mind-node-block').select('.invalid-path').attr('d', path[1])
      }
    }
    select('.mind-mapbox-editor-block')
      .style('top', `${top - ((height - data.data.foreignObjectHeight) / (isunderline ? 1 : 2)) * k}px`)
      .style('left', `${left + (spacingL - data.style.margin._l) * k + offsetX * k}px`)
    container.select('.select-rect-border')
      .attr('width', widthRect + (data.style.strokeWidth / 2 + 3) * 2)
      .attr('height', heightRect + (data.style.strokeWidth / 2 + 3) * 2)
      .attr('x', (data.x + offsetX - data.style.strokeWidth / 2 - 3))
      .attr('y', data.y - data.style.strokeWidth / 2 - 3 - (heightRect - data.height) / (isunderline ? 1 : 2))
    if (data.data.latex) {
      container.select(`#${id}`).select('.x-mind-node-text').attr('width', (width + latexWidth)).attr('height', Math.max(height, latexHeight))
      container.select(`#${id}`).select('.x-mind-node-text').select('.latex-span').style('margin-left', `${width + offsetX}px`)
    }
    if (data.data.link) {
      container.select(`#${id}`).select('.xmind-node-link').attr('transform', `translate(${widthRect - data.width + offsetX}, ${-data.data.foreignObjectHeight / 2 + data.style.linkSize / 2})`)
    }
    if (data.data.topiclink) {
      container.select(`#${id}`).select('.xmind-node-topiclink').attr('transform', `translate(${widthRect - data.width + offsetX}, ${-data.data.foreignObjectHeight / 2 + data.style.linkSize / 2})`)
    }
    if (data.data.comment) {
      container.select(`#${id}`).select('.xmind-node-comment').attr('transform', `translate(${widthRect - data.width + offsetX}, ${-data.data.foreignObjectHeight / 2 + data.style.linkSize / 2})`)
    }
    if (data.data.tag) {
      container.select(`#${id}`).select('.xmind-node-tags').attr('transform', () => {
        return `translate(${spacingL + offsetX}, ${heightRect - spacingB - (heightRect - data.height) / (isunderline ? 1 : 2)})`
      })
    }
    if (data.data.imageInfo) {
      container.select(`#${id}`).select('.xmind-node-image').select('image').attr('x', data.x + offsetX + (widthRect - data.data.imageInfo.width) / 2).attr('y', data.y - (heightRect - data.height) / 2 + spacingT)
    }
    if (data.data.isTask) {
      container.select(`#${id}`).select('.task-checkbox').attr('transform', () => {
        return `translate(${spacingL - data.style.margin._l + offsetX}, 0)`
      })
    }
    if (data.serialNumber) {
      container.select(`#${id}`).select('.node-serial-numner').attr('x', () => {
        const taskSpacing = data.data.isTask ? data.style.linkSize + 8 : 0
        const markWidth = data.markWidth || 0
        return data.x + data.style.margin._l + taskSpacing + markWidth + offsetX
      })
        .attr('y', () => {
          return data.y + data.height - (data.data.foreignObjectHeight + data.serialNumberHeight) / 2 - data.style.margin._b
        })
    }
    if (data.data.marks?.length) {
      container.select(`#${id}`).select('.xmind-node-mark').attr('transform', () => {
        return `translate(${spacingL - data.style.margin._l + offsetX}, 0)`
      })
    }
  }
}

export function showNodeHoverBorderPath (selection) {
  if (selection.select('.select-rect-border').empty()) {
    if (mindOutContainer.select('.node-hover-border-path').empty()) {
      mindOutContainer.append('g').attr('class', 'node-hover-border-path')
        .attr('fill', 'transparent')
        .style('pointer-events', 'none')
        .append('rect')
        .attr('stroke', '#2EBDFF90')
        .attr('stroke-width', 2)
        .attr('rx', 4)
        .attr('ry', 4)
        .attr('fill', 'transparent')
    }
    const data = selection.datum()
    const { width, height, x, y, style } = data
    const spacing = (style.strokeWidth / 2 + 3)
    mindOutContainer.select('.node-hover-border-path').style('display', 'block')
      .select('rect')
      .attr('x', x - spacing)
      .attr('y', y - spacing)
      .attr('width', width + spacing * 2)
      .attr('height', height + spacing * 2)
  }

  selection.select('.expand-circle')
    .transition().duration(300).attr('r', 6).attr('opacity', 1)
    .each(function () {
      select(this.parentNode).select('.expand-path').transition().duration(300).attr('opacity', 1)
    })
}

export function hiddenNodeHoverBorderPath (selection) {
  mindOutContainer.select('.node-hover-border-path').style('display', 'none')

  selection && selection.select('.expand-circle').transition().duration(300).attr('r', 4).attr('opacity', 0)
  selection && selection.select('.expand-path').transition().duration(300).attr('opacity', 0)
}
